// ============================================================================
// JigServer
// The JigServer provides all of the server side game play implementation
//
// CLH April-June 1998
// ============================================================================
package com.chrishobson.jiggle;

import java.net.*;
import java.util.*;
import java.awt.Point;

public class JigServer extends Thread {

  public static final int NONE  = 0;
  public static final int RIGHT = 1;
  public static final int LEFT  = 2;
  public static final int QUIT  = 3;
  public static final int JOIN  = 4;
  public static final int DIE   = 5;
  public static final int DEAD  = 6;
  public static final int KILLED  = 7;

  // Colours for joining worms. Allocated based on the Id of the new
  // worm. If more worms play than colours then the colours are reused
  // colours are allocated when a Jiggler becomes a listener not when it
  // becomes a player, so there maybe missing colours on a play board.
  private static String m_cols[]={"R0G255B0","R0G0B255", "R255G255B255",
				  "R255G0B255","R0G255B255"};

  // ==========================================================================
  // Creates a new gaming server with the specifed tuning parameters
  // ==========================================================================
  public  JigServer(int x, int y, int max_food, int max_block) {
    m_x = x;
    m_y = y;
    m_max_food = max_food;
    m_max_block = max_block;
    // Create the playing area
    m_board = new int[m_x][m_y];
    m_threads = new JigVector();
    m_worms = new JigVector();
    m_food = new int[m_max_food][2];
    m_block = new int[m_max_block][2];
    for(int f = 0; f < m_max_food; ++f) {
      // Flag the slot as not indicating a food location
      // by using -1 for the X coordinate
      m_food[f][0] = -1;
    }
    // Blocks are create once at the start, unlike food which
    // is created as it is eaten.
    make_blocks();
  }

  // ==========================================================================
  // Generates a random integer in the range 0 -> limit -1 .
  // Generates pseudo-random numbers, the lower limit is set
  // to 0 to allow the values to easily be used as array indexes.
  // ==========================================================================
  public final int rand(int limit) {
    return Math.abs(m_random.nextInt()) % limit;
  }

  // ==========================================================================
  // Create a new JigServerThread which is associated with the socket
  // ==========================================================================
  public void add(Socket s) {
    JigServerThread t = new JigServerThread(s,this);
    t.set_id(m_threads.addObj(t));
    t.start();
  }

  // ==========================================================================
  // Send the game parameter sizes to the specified socket
  // ==========================================================================
  public void send_sizes(JigSocket sock) {
    sock.send("SX" + m_x +"Y" + m_y + "F" + m_max_food + "B" + m_max_block);
  }

  // ==========================================================================
  // The servers run loop
  // ==========================================================================
  public void run() {
    // The server runs forever and is very simple. It first
    // loops through all of the players and processes any game
    // requests (Left,Right,Quit). This results in a completed
    // command string a second pass though all of the active games to
    // send the completed game move string to them finally a
    // 3rd pass is made to process any game join requests
    // where the entire game state is sent to any joining game
    // and a join instruction is sent to all of the other gaming
    // servers. Only a single player is allowed to join during
    // each full pass although
    StringBuffer command = new StringBuffer(1024);
    Point pos = new Point();
    while(true) {
      // Set the length of the command buffer to zero so that
      // it can start to be filled with the new commands
      command.setLength(0);
      // Flag which is set to true when a new player joins, this
      // ensure that only one player joins per pass
      boolean joined = false;
      // Now loop through each of the listeners and process their
      // pending commands. Any player who has not requested a move
      // will be moved forward.
      int size = m_threads.size();
      for(int id =0; id < size; ++id) {
	JigServerThread t = (JigServerThread)m_threads.elementAt(id);
	// If the thread exists
	if(t != null) {
	  int com = t.get_command();
	  boolean player = t.is_player();
	  JigWorm worm = null;
	  if(player == true) {
	    worm = t.get_worm();
	  }
	  if(com == DIE || com == QUIT) {
	    // This thread is waiting to die so signal to the other
	    // listeners that the player is dead and that they should
	    // remove the player from the screen and forget it
	    command.append("W").append(id).append("Q");
	    // set the worm as DEAD so we can clean up later
	    t.set_command(DEAD);
	    // Cancel the worm, if any
	    if(worm != null) {
	      int len = worm.get_length();
	      for(int i = 0; i <= len; ++i) {
		worm.get_pos(i,pos);
		// Mark square as empty
		m_board[pos.x][pos.y] = 0;
	      }
	    }
	  } else if(player == true) {
	    char com_str;
	    boolean grew = false;
	    if(com == LEFT) {
	      worm.move_left();
	      com_str = 'L';
	    } else if(com == RIGHT) {
	      worm.move_right();
	      com_str = 'R';
	    } else {
	      worm.move_forward();
	      com_str = 'F';
	    }
	    t.set_command(NONE);
	    // Worm has moved update the board to cancel out the last tail
	    // position
	    worm.get_last_tail(pos);
	    m_board[pos.x][pos.y] = 0;
	    // Now check the head for a collision or for food
	    worm.get_head(pos);
	    // System.out.println(pos.x + " " + pos.y);
	    int v;
	    int x = pos.x;
	    int y = pos.y;
	    if(x < 0 || x >= m_x || y < 0 || y >= m_y) {
	      // Ran off board limits, treat as death
	      v = -1;
	    } else {
	      v = m_board[pos.x][pos.y];
	    }
	    // If there is something in the way
	    if(v != 0) {
	      // is it food ?
	      if(v < -99) {
		grew = true;
		worm.grow();
		m_food[-100 -v][0] = -1;
		m_num_food--;
		m_board[pos.x][pos.y] = id + 1;
	      } else {
		command.append("W").append(id).append("D");
		t.set_command(KILLED);
		t.leave_game();
		// Cancel the worm positions, skip head it hasn't
		// been updated yet.
		int len = worm.get_length();
		for(int i = 1; i <= len; ++i) {
		  worm.get_pos(i,pos);
		  // Mark square as empty
		  m_board[pos.x][pos.y] = 0;
		}
	      }
	    }
	    if(t.get_command() == NONE) {
	      // Update baord for new head. If eaten food then
	      // the food has already been updated
	      worm.get_head(pos);
	      m_board[pos.x][pos.y] = id + 1;
	      command.append("W").append(id).append(com_str);
	      if(grew) {
		command.append("W").append(id).append("G");
	      }
	    }
	  } else if(com == JOIN && joined == false && player == false) {
	    // We are going to join at X0 X1 so check that X0,X1,X2 are
	    // all empty to prevent joining on a worm that has just
	    // joined which leads to a rapid death after one move.
	    if(m_board[0][m_y-1] == 0 && m_board[1][m_y-1] == 0 &&
	       m_board[2][m_y-1] == 0) {
	      String wormstr = "L1" + (m_cols[id%m_cols.length]) +
		"X1Y"+(m_y-1)+"X0Y"+(m_y-1);
	      t.join_game(wormstr);
	      command.append("J"+id+wormstr);
	      joined = true;
	      t.set_command(NONE);
	    }
	  }
	}
      }
      // See if any more food needs to be made and add it to the
      // board.
      make_food(command);
      // Send the current command string to all clients so that they
      // can update.
      broadcast(command);
      try {
	// Sleep for 1/4 second so that we get 4 moves per second which
	// results in a nice smooth game
	sleep(250);
      } catch (InterruptedException e) {
      }
      // Now tidy any worms which were set to DEAD on the last
      // command pass
      tidy();
      // Debug method to dump the play board after each loop
      // to allow a check that it contains the correct data
      // dump_board();
    }
  }

  // ==========================================================================
  // Tidy up any dead jigglers
  // ==========================================================================
  private void tidy() {
    int size = m_threads.size();
    for(int id =0; id < size; ++id) {
      JigServerThread t = (JigServerThread)m_threads.elementAt(id);
      // If the thread exists
      if(t != null && t.get_command() == DEAD) {
	m_threads.set_null(id);
	m_worms.set_null(id);
      }
    }
  }

  // ==========================================================================
  // Broadcast the command string to all listeners
  // ==========================================================================
  private void broadcast(StringBuffer command) {
    // Do nothing is the command is empty
    if(command.length() > 0) {
      // Convert the buffer into a string. The buffer is used to build
      // the command because it can grow. However most of the Java
      // methods require strings because it is a string (once created) is
      // constant.
      String s = command.toString();
      int num = m_threads.size();
      for(int id = 0; id < num; ++id) {
	JigServerThread t = (JigServerThread)m_threads.elementAt(id);
	// If the thread exists and has become a full listener
	if(t != null && t.is_listener()) {
	  t.send(s);
	}
      }
    }
  }

  // ==========================================================================
  // Creates a new food item if the food is below m_max_food.
  // Food is created when the food is below the maximum limit
  // but it is only made once every 15 calls (approximately) to prevent
  // food always appearing as soon as it is consumed.
  // ==========================================================================
  private void make_food(StringBuffer command) {
    if(m_num_food < m_max_food) {
      // Make food every 15 calls approximately
      if(rand(15) == 0) {
	boolean made = false;
	// We have to loop to find a free slot for food
	// we have at most 5 attempts at finding a slot
	// before giving in. We don't want to hang up the
	// game just to add food.
	for(int i = 0; i < 5 && made == false; ++i) {
          // Make the X position, ensure that food never
	  // appears on the left/right edge of the screen
	  // otherwise the worm can grow to a position
          // off the screen
	  int x = rand(m_x - 2) + 1;
	  // Again make a randon Y which is not on the
	  // top/bottom edge of the screen.
	  int y = rand(m_y - 2) + 1;
	  // Check if the slot is free and a valid location
	  if(check_slot(x,y)) {
	    // We need to add the food to the food array
	    for(int f = 0; f < m_max_food; ++f) {
	      // If this slot is free then we'll store the
	      // x,y in this slot
	      if(m_food[f][0] == -1) {
		m_food[f][0] = x;
		m_food[f][1] = y;
		// Food is stored in the main game board as
		// -100 - f ie. Food slot 1 is indicated
		// by storing -101 food slot 0 is therefore
		// -100. This allows food to be spotted during play
		// as a square contains an integer of <= -100
		m_board[x][y] = -100 - f;
		++m_num_food;
		made = true;
		break;
	      }
	    }
	    // Finally we need to send a New Food command
	    // to the play screens
	    command.append("FX").append(x).append("Y").append(y);
	    // Display it on screen to add final presentaion
	    // this line could be removed from a later version
	    System.out.println("Food made at " + x + " " + y);
	  }
	}
      }
    }
  }
  // ==========================================================================
  // Creates the m_max_block blocks.
  // ==========================================================================
  private void make_blocks() {
    for(int b = 0; b < m_max_block; ++b) {
      while(true) {
	int x = rand(m_x);
	// Avoid puting blocks on the bottom row which
	// is where worms join
	int y = rand(m_y - 1);
	// We have found a slot
	if(check_slot(x,y)) {
	  // We add the block to the block array and the board
	  m_board[x][y] = -1;
	  m_block[b][0] = x;
	  m_block[b][1] = y;
	  // Break from while loop, block 'b' created
	  break;
	}
      }
    }
  }

  // ==========================================================================
  // Check if the slot is free and a valid location.
  // Used by make_food and make_blocks to check if
  // the slot is both free and that there is no food
  // or a block next to this slot. It is OK for a worm
  // to be located next to the slot.
  // ==========================================================================
  //
  // Array of offsets used to step left, right, up and
  // down relative to candidate position
  private static int s_offsets[] = {-1,0,1,0,0,-1,0,1};

  private boolean check_slot(int x, int y) {
    boolean ok = false;
    // If slot is free
    if(m_board[x][y] == 0) {
      // Now assume everything will be OK
      ok = true;
      for(int o = 0; o < 7;) {
	int t_x = x + s_offsets[o++];
	int t_y = y + s_offsets[o++];
	// Check we haven't stepped off the board
	if(t_x > -1 && t_x < m_x && t_y > -1 && t_y < m_y) {
	  // If < 0 then slot contains food or a block
	  if(m_board[t_x][t_y] < 0) {
	    // Leave no, no point carrying on
	      return false;
	  }
	}
      }
    }
    return ok;
  }

  // ==========================================================================
  // Sends the current game positions to the socket. This is used to
  // initialise a new listener with the current game state.
  // ==========================================================================
  public void send_game(JigSocket sock) {
    int i;
    m_game.setLength(0);
    // First construct the food location instructions
    for(i = 0; i < m_max_food; ++i) {
      if(m_food[i][0] != -1) {
	m_game.append("FX").append(m_food[i][0]).
	  append("Y").append(m_food[i][1]);
      }
    }
    for(i = 0; i < m_max_block; ++i) {
      m_game.append("BX").append(m_block[i][0]).
	  append("Y").append(m_block[i][1]);
    }

    // Add in any active worms via the 'J'oin command
    int size = m_threads.size();
    for(int id = 0; id < size; ++id) {
      JigServerThread t = (JigServerThread)m_threads.elementAt(id);
      // If the thread exists
      if(t != null) {
	// And they have a worm
	JigWorm worm = t.get_worm();
	if(worm != null) {
	  // Add the id
	  m_game.append("J").append(id);
	  // Then get the worm to add in its details
	  worm.add_buffer(m_game);
	}
      }
    }
    sock.send(m_game.toString());
  }

  // ==========================================================================
  // Dump out the board so I can see that everything is tracking
  // correctly. Just a debug function, the call to it is commented out
  // ==========================================================================
  @SuppressWarnings("unused")
  private void dump_board() {
    char c;
    System.out.println("===============================================");
    for(int y = 0; y < m_y; ++y) {
      for(int x = 0; x < m_x; ++x) {
	c = ' ';
	if(m_board[x][y] == -1) {
	  c = 'X';
	} else if(m_board[x][y] < -99) {
	  c = 'F';
	} else if(m_board[x][y] != 0) {
	  c = 'W';
	}
	System.out.print(c);
      }
      System.out.println();
    }
    System.out.println("===============================================");
  }

  // ==========================================================================
  // JigServer data members
  // ==========================================================================

  // The board is an array of integers
  int m_board[][];
  int m_x;
  int m_y;
  int m_max_food;
  int m_max_block;
  // The array of food coordinates. Negative X value indicates
  // no food at the location. This array must remain syncronised
  // with the m_board array. It exists to allow allow food positions
  // to be quickly sent to clients when they first join the game.
  int m_food[][];
  // How many food slots are in use
  int m_num_food;

  // The array of block coordinates.
  int m_block[][];

  // The random generator
  Random m_random = new Random();

  // A fairly large buffer for sending the game status to new
  // clients. It will grow if needed and 2K is plenty unless
  // the entire group plays at the same time.
  StringBuffer m_game = new StringBuffer(2048);

  JigVector m_threads;
  JigVector m_worms;
}

